@@ -158,6 +158,7 @@ h2 .scenario, a span.label.scenario { |
||
158 | 158 |
} |
159 | 159 |
|
160 | 160 |
// Bootstrappy color styles |
161 |
+ |
|
161 | 162 |
.color-danger { |
162 | 163 |
color: #d9534f; |
163 | 164 |
} |
@@ -0,0 +1,9 @@ |
||
1 |
+.scenario-import { |
|
2 |
+ .danger { |
|
3 |
+ color: red; |
|
4 |
+ font-weight: strong; |
|
5 |
+ border: 1px solid red; |
|
6 |
+ padding: 10px; |
|
7 |
+ margin: 10px 0; |
|
8 |
+ } |
|
9 |
+} |
@@ -29,7 +29,7 @@ module AssignableTypes |
||
29 | 29 |
const_get(:TYPES).include?(type) |
30 | 30 |
end |
31 | 31 |
|
32 |
- def build_for_type(type, user, attributes) |
|
32 |
+ def build_for_type(type, user, attributes = {}) |
|
33 | 33 |
attributes.delete(:type) |
34 | 34 |
|
35 | 35 |
if valid_type?(type) |
@@ -0,0 +1,13 @@ |
||
1 |
+module HasGuid |
|
2 |
+ extend ActiveSupport::Concern |
|
3 |
+ |
|
4 |
+ included do |
|
5 |
+ before_save :make_guid |
|
6 |
+ end |
|
7 |
+ |
|
8 |
+ protected |
|
9 |
+ |
|
10 |
+ def make_guid |
|
11 |
+ self.guid = SecureRandom.hex unless guid.present? |
|
12 |
+ end |
|
13 |
+end |
@@ -12,6 +12,7 @@ class Agent < ActiveRecord::Base |
||
12 | 12 |
include JSONSerializedField |
13 | 13 |
include RDBMSFunctions |
14 | 14 |
include WorkingHelpers |
15 |
+ include HasGuid |
|
15 | 16 |
|
16 | 17 |
markdown_class_attributes :description, :event_description |
17 | 18 |
|
@@ -1,22 +1,18 @@ |
||
1 | 1 |
class Scenario < ActiveRecord::Base |
2 |
- attr_accessible :name, :agent_ids, :description, :public |
|
2 |
+ include HasGuid |
|
3 |
+ |
|
4 |
+ attr_accessible :name, :agent_ids, :description, :public, :source_url |
|
3 | 5 |
|
4 | 6 |
belongs_to :user, :counter_cache => :scenario_count, :inverse_of => :scenarios |
5 | 7 |
has_many :scenario_memberships, :dependent => :destroy, :inverse_of => :scenario |
6 | 8 |
has_many :agents, :through => :scenario_memberships, :inverse_of => :scenarios |
7 | 9 |
|
8 |
- before_save :make_guid |
|
9 |
- |
|
10 | 10 |
validates_presence_of :name, :user |
11 | 11 |
|
12 | 12 |
validate :agents_are_owned |
13 | 13 |
|
14 | 14 |
protected |
15 | 15 |
|
16 |
- def make_guid |
|
17 |
- self.guid = SecureRandom.hex unless guid.present? |
|
18 |
- end |
|
19 |
- |
|
20 | 16 |
def agents_are_owned |
21 | 17 |
errors.add(:agents, "must be owned by you") unless agents.all? {|s| s.user == user } |
22 | 18 |
end |
@@ -4,6 +4,7 @@ class ScenarioImport |
||
4 | 4 |
include ActiveModel::Callbacks |
5 | 5 |
include ActiveModel::Validations::Callbacks |
6 | 6 |
|
7 |
+ DANGEROUS_AGENT_TYPES = %w[Agents::ShellCommandAgent] |
|
7 | 8 |
URL_REGEX = /\Ahttps?:\/\//i |
8 | 9 |
|
9 | 10 |
attr_accessor :file, :url, :data, :do_import |
@@ -33,19 +34,54 @@ class ScenarioImport |
||
33 | 34 |
@existing_scenario ||= user.scenarios.find_by_guid(parsed_data["guid"]) |
34 | 35 |
end |
35 | 36 |
|
37 |
+ def dangerous? |
|
38 |
+ (parsed_data['agents'] || []).any? { |agent| DANGEROUS_AGENT_TYPES.include?(agent['type']) } |
|
39 |
+ end |
|
40 |
+ |
|
36 | 41 |
def parsed_data |
37 |
- @parsed_data |
|
42 |
+ @parsed_data ||= data && JSON.parse(data) rescue {} |
|
38 | 43 |
end |
39 | 44 |
|
40 | 45 |
def do_import? |
41 | 46 |
do_import == "1" |
42 | 47 |
end |
43 | 48 |
|
44 |
- def import! |
|
49 |
+ def import!(options = {}) |
|
50 |
+ guid = parsed_data['guid'] |
|
51 |
+ description = parsed_data['description'] |
|
52 |
+ name = parsed_data['name'] |
|
53 |
+ agents = parsed_data['agents'] |
|
54 |
+ links = parsed_data['links'] |
|
55 |
+ source_url = parsed_data['source_url'].presence || nil |
|
56 |
+ @scenario = user.scenarios.where(:guid => guid).first_or_initialize |
|
57 |
+ @scenario.update_attributes!(:name => name, :description => description, |
|
58 |
+ :source_url => source_url, :public => false) |
|
59 |
+ |
|
60 |
+ unless options[:skip_agents] |
|
61 |
+ created_agents = agents.map do |agent_data| |
|
62 |
+ agent = @scenario.agents.find_by(:guid => agent_data['guid']) || Agent.build_for_type(agent_data['type'], user) |
|
63 |
+ agent.guid = agent_data['guid'] |
|
64 |
+ agent.attributes = { :name => agent_data['name'], |
|
65 |
+ :schedule => agent_data['schedule'], |
|
66 |
+ :keep_events_for => agent_data['keep_events_for'], |
|
67 |
+ :propagate_immediately => agent_data['propagate_immediately'], |
|
68 |
+ :disabled => agent_data['disabled'], |
|
69 |
+ :options => agent_data['options'], |
|
70 |
+ :scenario_ids => [@scenario.id] } |
|
71 |
+ agent.save! |
|
72 |
+ agent |
|
73 |
+ end |
|
74 |
+ |
|
75 |
+ links.each do |link| |
|
76 |
+ receiver = created_agents[link['receiver']] |
|
77 |
+ source = created_agents[link['source']] |
|
78 |
+ receiver.sources << source unless receiver.sources.include?(source) |
|
79 |
+ end |
|
80 |
+ end |
|
45 | 81 |
end |
46 | 82 |
|
47 | 83 |
def scenario |
48 |
- existing_scenario |
|
84 |
+ @scenario || @existing_scenario |
|
49 | 85 |
end |
50 | 86 |
|
51 | 87 |
protected |
@@ -65,10 +101,12 @@ class ScenarioImport |
||
65 | 101 |
def validate_data |
66 | 102 |
if data.present? |
67 | 103 |
@parsed_data = JSON.parse(data) rescue {} |
68 |
- if (%w[name guid] - @parsed_data.keys).length > 0 |
|
104 |
+ if (%w[name guid agents] - @parsed_data.keys).length > 0 |
|
69 | 105 |
errors.add(:base, "The provided data does not appear to be a valid Scenario.") |
70 | 106 |
self.data = nil |
71 | 107 |
end |
108 |
+ else |
|
109 |
+ @parsed_data = nil |
|
72 | 110 |
end |
73 | 111 |
end |
74 | 112 |
|
@@ -32,11 +32,20 @@ |
||
32 | 32 |
}); |
33 | 33 |
</script> |
34 | 34 |
|
35 |
+ <% if @scenario_import.dangerous? %> |
|
36 |
+ <div class="danger"> |
|
37 |
+ This Scenario contains one or more potentially dangerous Agents. |
|
38 |
+ These may be able to run local commands or execute code. |
|
39 |
+ Please be sure that you understand the above Agent configurations before importing! |
|
40 |
+ </div> |
|
41 |
+ <% end %> |
|
42 |
+ |
|
35 | 43 |
<% if @scenario_import.existing_scenario.present? %> |
36 |
- <strong> |
|
37 |
- This Scenario already exists on your Huginn. |
|
38 |
- If you continue, the import will overwrite your existing <span class='label label-info scenario'><%= @scenario_import.existing_scenario.name %></span> Scenario. |
|
39 |
- </strong> |
|
44 |
+ <div class="danger"> |
|
45 |
+ This Scenario already exists in your system. |
|
46 |
+ If you continue, the import will overwrite your existing |
|
47 |
+ <span class='label label-info scenario'><%= @scenario_import.existing_scenario.name %></span> Scenario and the Agents in it. |
|
48 |
+ </div> |
|
40 | 49 |
<% end %> |
41 | 50 |
|
42 | 51 |
<div class="checkbox"> |
@@ -1,4 +1,4 @@ |
||
1 |
-<div class='container'> |
|
1 |
+<div class='container scenario-import'> |
|
2 | 2 |
<div class='row'> |
3 | 3 |
<div class='col-md-12'> |
4 | 4 |
<% if @scenario_import.errors.any? %> |
@@ -13,6 +13,7 @@ |
||
13 | 13 |
<tr> |
14 | 14 |
<th>Name</th> |
15 | 15 |
<th>Agents</th> |
16 |
+ <th>Public</th> |
|
16 | 17 |
<th></th> |
17 | 18 |
</tr> |
18 | 19 |
|
@@ -22,6 +23,7 @@ |
||
22 | 23 |
<%= link_to(scenario.name, scenario, class: "label label-info") %> |
23 | 24 |
</td> |
24 | 25 |
<td><%= link_to pluralize(scenario.agents.count, "agent"), scenario %></td> |
26 |
+ <td><%= scenario.public? ? "yes" : "no" %></td> |
|
25 | 27 |
<td> |
26 | 28 |
<div class="btn-group btn-group-xs" style="float: right"> |
27 | 29 |
<%= link_to 'Show', scenario, class: "btn btn-default" %> |
@@ -1,6 +1,6 @@ |
||
1 | 1 |
class AddIndicesToScenarios < ActiveRecord::Migration |
2 | 2 |
def change |
3 |
- add_index :scenarios, [:user_id, :guid] |
|
3 |
+ add_index :scenarios, [:user_id, :guid], :unique => true |
|
4 | 4 |
add_index :scenario_memberships, :agent_id |
5 | 5 |
add_index :scenario_memberships, :scenario_id |
6 | 6 |
end |
@@ -0,0 +1,15 @@ |
||
1 |
+class AddGuidToAgents < ActiveRecord::Migration |
|
2 |
+ class Agent < ActiveRecord::Base; end |
|
3 |
+ |
|
4 |
+ def change |
|
5 |
+ add_column :agents, :guid, :string |
|
6 |
+ |
|
7 |
+ Agent.find_each do |agent| |
|
8 |
+ agent.update_attribute :guid, SecureRandom.hex |
|
9 |
+ end |
|
10 |
+ |
|
11 |
+ change_column_null :agents, :guid, false |
|
12 |
+ |
|
13 |
+ add_index :agents, :guid |
|
14 |
+ end |
|
15 |
+end |
@@ -11,7 +11,7 @@ |
||
11 | 11 |
# |
12 | 12 |
# It's strongly recommended that you check this file into your version control system. |
13 | 13 |
|
14 |
-ActiveRecord::Schema.define(version: 20140602014917) do |
|
14 |
+ActiveRecord::Schema.define(version: 20140605032822) do |
|
15 | 15 |
|
16 | 16 |
create_table "agent_logs", force: true do |t| |
17 | 17 |
t.integer "agent_id", null: false |
@@ -42,8 +42,10 @@ ActiveRecord::Schema.define(version: 20140602014917) do |
||
42 | 42 |
t.integer "keep_events_for", default: 0, null: false |
43 | 43 |
t.boolean "propagate_immediately", default: false, null: false |
44 | 44 |
t.boolean "disabled", default: false, null: false |
45 |
+ t.string "guid", null: false |
|
45 | 46 |
end |
46 | 47 |
|
48 |
+ add_index "agents", ["guid"], name: "index_agents_on_guid", using: :btree |
|
47 | 49 |
add_index "agents", ["schedule"], name: "index_agents_on_schedule", using: :btree |
48 | 50 |
add_index "agents", ["type"], name: "index_agents_on_type", using: :btree |
49 | 51 |
add_index "agents", ["user_id", "created_at"], name: "index_agents_on_user_id_and_created_at", using: :btree |
@@ -111,7 +113,7 @@ ActiveRecord::Schema.define(version: 20140602014917) do |
||
111 | 113 |
t.string "source_url" |
112 | 114 |
end |
113 | 115 |
|
114 |
- add_index "scenarios", ["user_id", "guid"], name: "index_scenarios_on_user_id_and_guid", using: :btree |
|
116 |
+ add_index "scenarios", ["user_id", "guid"], name: "index_scenarios_on_user_id_and_guid", unique: true, using: :btree |
|
115 | 117 |
|
116 | 118 |
create_table "user_credentials", force: true do |t| |
117 | 119 |
t.integer "user_id", null: false |
@@ -46,7 +46,7 @@ class AgentsExporter |
||
46 | 46 |
:keep_events_for => agent.keep_events_for, |
47 | 47 |
:propagate_immediately => agent.propagate_immediately, |
48 | 48 |
:disabled => agent.disabled, |
49 |
- :source_system_agent_id => agent.id, |
|
49 |
+ :guid => agent.guid, |
|
50 | 50 |
:options => agent.options |
51 | 51 |
} |
52 | 52 |
end |
@@ -4,6 +4,7 @@ jane_website_agent: |
||
4 | 4 |
events_count: 1 |
5 | 5 |
schedule: "5pm" |
6 | 6 |
name: "ZKCD" |
7 |
+ guid: <%= SecureRandom.hex %> |
|
7 | 8 |
options: <%= { |
8 | 9 |
:url => "http://trailers.apple.com/trailers/home/rss/newtrailers.rss", |
9 | 10 |
:expected_update_period_in_days => 2, |
@@ -20,6 +21,7 @@ bob_website_agent: |
||
20 | 21 |
events_count: 1 |
21 | 22 |
schedule: "midnight" |
22 | 23 |
name: "ZKCD" |
24 |
+ guid: <%= SecureRandom.hex %> |
|
23 | 25 |
options: <%= { |
24 | 26 |
:url => "http://xkcd.com", |
25 | 27 |
:expected_update_period_in_days => 2, |
@@ -35,6 +37,7 @@ bob_weather_agent: |
||
35 | 37 |
user: bob |
36 | 38 |
schedule: "midnight" |
37 | 39 |
name: "SF Weather" |
40 |
+ guid: <%= SecureRandom.hex %> |
|
38 | 41 |
keep_events_for: 45 |
39 | 42 |
options: <%= { |
40 | 43 |
:location => 94102, |
@@ -48,6 +51,7 @@ jane_weather_agent: |
||
48 | 51 |
user: jane |
49 | 52 |
schedule: "midnight" |
50 | 53 |
name: "SF Weather" |
54 |
+ guid: <%= SecureRandom.hex %> |
|
51 | 55 |
keep_events_for: 30 |
52 | 56 |
options: <%= { |
53 | 57 |
:location => 94103, |
@@ -60,6 +64,7 @@ jane_rain_notifier_agent: |
||
60 | 64 |
type: Agents::TriggerAgent |
61 | 65 |
user: jane |
62 | 66 |
name: "Jane's Rain Watcher" |
67 |
+ guid: <%= SecureRandom.hex %> |
|
63 | 68 |
options: <%= { |
64 | 69 |
:expected_receive_period_in_days => "2", |
65 | 70 |
:rules => [{ |
@@ -74,6 +79,7 @@ bob_rain_notifier_agent: |
||
74 | 79 |
type: Agents::TriggerAgent |
75 | 80 |
user: bob |
76 | 81 |
name: "Bob's Rain Watcher" |
82 |
+ guid: <%= SecureRandom.hex %> |
|
77 | 83 |
options: <%= { |
78 | 84 |
:expected_receive_period_in_days => "2", |
79 | 85 |
:rules => [{ |
@@ -88,6 +94,7 @@ bob_twitter_user_agent: |
||
88 | 94 |
type: Agents::TwitterUserAgent |
89 | 95 |
user: bob |
90 | 96 |
name: "Bob's Twitter User Watcher" |
97 |
+ guid: <%= SecureRandom.hex %> |
|
91 | 98 |
options: <%= { |
92 | 99 |
:username => "tectonic", |
93 | 100 |
:expected_update_period_in_days => "2", |
@@ -101,3 +108,4 @@ bob_manual_event_agent: |
||
101 | 108 |
type: Agents::ManualEventAgent |
102 | 109 |
user: bob |
103 | 110 |
name: "Bob's event testing agent" |
111 |
+ guid: <%= SecureRandom.hex %> |
@@ -20,7 +20,7 @@ describe AgentsExporter do |
||
20 | 20 |
Time.parse(data[:exported_at]).should be_within(2).of(Time.now.utc) |
21 | 21 |
data[:links].should == [{ :source => 0, :receiver => 1 }] |
22 | 22 |
data[:agents].should == agent_list.map { |agent| exporter.agent_as_json(agent) } |
23 |
- data[:agents].all? { |agent_json| agent_json[:source_system_agent_id] && agent_json[:type] && agent_json[:name] }.should be_true |
|
23 |
+ data[:agents].all? { |agent_json| agent_json[:guid].present? && agent_json[:type].present? && agent_json[:name].present? }.should be_true |
|
24 | 24 |
end |
25 | 25 |
|
26 | 26 |
it "does not output links to other agents" do |
@@ -1,5 +1,4 @@ |
||
1 | 1 |
require 'spec_helper' |
2 |
-require 'models/concerns/working_helpers' |
|
3 | 2 |
|
4 | 3 |
describe Agent do |
5 | 4 |
it_behaves_like WorkingHelpers |
@@ -122,6 +121,14 @@ describe Agent do |
||
122 | 121 |
stub(Agents::CannotBeScheduled).valid_type?("Agents::CannotBeScheduled") { true } |
123 | 122 |
end |
124 | 123 |
|
124 |
+ let(:new_instance) do |
|
125 |
+ agent = Agents::SomethingSource.new(:name => "some agent") |
|
126 |
+ agent.user = users(:bob) |
|
127 |
+ agent |
|
128 |
+ end |
|
129 |
+ |
|
130 |
+ it_behaves_like HasGuid |
|
131 |
+ |
|
125 | 132 |
describe ".default_schedule" do |
126 | 133 |
it "stores the default on the class" do |
127 | 134 |
Agents::SomethingSource.default_schedule.should == "2pm" |
@@ -1,7 +1,6 @@ |
||
1 | 1 |
# encoding: utf-8 |
2 | 2 |
|
3 | 3 |
require 'spec_helper' |
4 |
-require 'models/concerns/liquid_interpolatable' |
|
5 | 4 |
|
6 | 5 |
describe Agents::DataOutputAgent do |
7 | 6 |
it_behaves_like LiquidInterpolatable |
@@ -1,5 +1,4 @@ |
||
1 | 1 |
require 'spec_helper' |
2 |
-require 'models/concerns/liquid_interpolatable' |
|
3 | 2 |
|
4 | 3 |
describe Agents::HipchatAgent do |
5 | 4 |
it_behaves_like LiquidInterpolatable |
@@ -1,5 +1,4 @@ |
||
1 | 1 |
require 'spec_helper' |
2 |
-require 'models/concerns/liquid_interpolatable' |
|
3 | 2 |
|
4 | 3 |
describe Agents::HumanTaskAgent do |
5 | 4 |
it_behaves_like LiquidInterpolatable |
@@ -1,5 +1,4 @@ |
||
1 | 1 |
require 'spec_helper' |
2 |
-require 'models/concerns/liquid_interpolatable' |
|
3 | 2 |
|
4 | 3 |
describe Agents::JabberAgent do |
5 | 4 |
it_behaves_like LiquidInterpolatable |
@@ -1,5 +1,4 @@ |
||
1 | 1 |
require 'spec_helper' |
2 |
-require 'models/concerns/liquid_interpolatable' |
|
3 | 2 |
|
4 | 3 |
describe Agents::PeakDetectorAgent do |
5 | 4 |
it_behaves_like LiquidInterpolatable |
@@ -1,5 +1,4 @@ |
||
1 | 1 |
require 'spec_helper' |
2 |
-require 'models/concerns/liquid_interpolatable' |
|
3 | 2 |
|
4 | 3 |
describe Agents::PushbulletAgent do |
5 | 4 |
it_behaves_like LiquidInterpolatable |
@@ -1,5 +1,4 @@ |
||
1 | 1 |
require 'spec_helper' |
2 |
-require 'models/concerns/liquid_interpolatable' |
|
3 | 2 |
|
4 | 3 |
describe Agents::SlackAgent do |
5 | 4 |
it_behaves_like LiquidInterpolatable |
@@ -1,6 +1,4 @@ |
||
1 | 1 |
require 'spec_helper' |
2 |
-require 'models/concerns/liquid_interpolatable' |
|
3 |
- |
|
4 | 2 |
|
5 | 3 |
describe Agents::TranslationAgent do |
6 | 4 |
it_behaves_like LiquidInterpolatable |
@@ -1,5 +1,4 @@ |
||
1 | 1 |
require 'spec_helper' |
2 |
-require 'models/concerns/liquid_interpolatable' |
|
3 | 2 |
|
4 | 3 |
describe Agents::TriggerAgent do |
5 | 4 |
it_behaves_like LiquidInterpolatable |
@@ -1,6 +1,64 @@ |
||
1 | 1 |
require 'spec_helper' |
2 | 2 |
|
3 | 3 |
describe ScenarioImport do |
4 |
+ let(:guid) { "somescenarioguid" } |
|
5 |
+ let(:description) { "This is a cool Huginn Scenario that does something useful!" } |
|
6 |
+ let(:name) { "A useful Scenario" } |
|
7 |
+ let(:source_url) { "http://example.com/scenarios/2/export.json" } |
|
8 |
+ let(:weather_agent_options) { |
|
9 |
+ { |
|
10 |
+ 'api_key' => 'some-api-key', |
|
11 |
+ 'location' => '12345' |
|
12 |
+ } |
|
13 |
+ } |
|
14 |
+ let(:trigger_agent_options) { |
|
15 |
+ { |
|
16 |
+ 'expected_receive_period_in_days' => 2, |
|
17 |
+ 'rules' => [{ |
|
18 |
+ 'type' => "regex", |
|
19 |
+ 'value' => "rain|storm", |
|
20 |
+ 'path' => "conditions", |
|
21 |
+ }], |
|
22 |
+ 'message' => "Looks like rain!" |
|
23 |
+ } |
|
24 |
+ } |
|
25 |
+ let(:valid_parsed_data) do |
|
26 |
+ { |
|
27 |
+ :name => name, |
|
28 |
+ :description => description, |
|
29 |
+ :guid => guid, |
|
30 |
+ :source_url => source_url, |
|
31 |
+ :exported_at => 2.days.ago.utc.iso8601, |
|
32 |
+ :agents => [ |
|
33 |
+ { |
|
34 |
+ :type => "Agents::WeatherAgent", |
|
35 |
+ :name => "a weather agent", |
|
36 |
+ :schedule => "5pm", |
|
37 |
+ :keep_events_for => 14, |
|
38 |
+ :propagate_immediately => false, |
|
39 |
+ :disabled => false, |
|
40 |
+ :guid => "a-weather-agent", |
|
41 |
+ :options => weather_agent_options |
|
42 |
+ }, |
|
43 |
+ { |
|
44 |
+ :type => "Agents::TriggerAgent", |
|
45 |
+ :name => "listen for weather", |
|
46 |
+ :schedule => nil, |
|
47 |
+ :keep_events_for => 0, |
|
48 |
+ :propagate_immediately => true, |
|
49 |
+ :disabled => true, |
|
50 |
+ :guid => "a-trigger-agent", |
|
51 |
+ :options => trigger_agent_options |
|
52 |
+ } |
|
53 |
+ ], |
|
54 |
+ :links => [ |
|
55 |
+ { :source => 0, :receiver => 1 } |
|
56 |
+ ] |
|
57 |
+ } |
|
58 |
+ end |
|
59 |
+ let(:valid_data) { valid_parsed_data.to_json } |
|
60 |
+ let(:invalid_data) { { :name => "some scenario missing a guid" }.to_json } |
|
61 |
+ |
|
4 | 62 |
describe "initialization" do |
5 | 63 |
it "is initialized with an attributes hash" do |
6 | 64 |
ScenarioImport.new(:url => "http://google.com").url.should == "http://google.com" |
@@ -9,8 +67,6 @@ describe ScenarioImport do |
||
9 | 67 |
|
10 | 68 |
describe "validations" do |
11 | 69 |
subject { ScenarioImport.new } |
12 |
- let(:valid_json) { { :name => "some scenario", :guid => "someguid" }.to_json } |
|
13 |
- let(:invalid_json) { { :name => "some scenario missing a guid" }.to_json } |
|
14 | 70 |
|
15 | 71 |
it "is not valid when none of file, url, or data are present" do |
16 | 72 |
subject.should_not be_valid |
@@ -20,7 +76,7 @@ describe ScenarioImport do |
||
20 | 76 |
|
21 | 77 |
describe "data" do |
22 | 78 |
it "should be invalid with invalid data" do |
23 |
- subject.data = invalid_json |
|
79 |
+ subject.data = invalid_data |
|
24 | 80 |
subject.should_not be_valid |
25 | 81 |
subject.should have(1).error_on(:base) |
26 | 82 |
|
@@ -33,7 +89,7 @@ describe ScenarioImport do |
||
33 | 89 |
end |
34 | 90 |
|
35 | 91 |
it "should be valid with valid data" do |
36 |
- subject.data = valid_json |
|
92 |
+ subject.data = valid_data |
|
37 | 93 |
subject.should be_valid |
38 | 94 |
end |
39 | 95 |
end |
@@ -47,14 +103,14 @@ describe ScenarioImport do |
||
47 | 103 |
end |
48 | 104 |
|
49 | 105 |
it "should be invalid when the referenced url doesn't contain a scenario" do |
50 |
- stub_request(:get, "http://example.com/scenarios/1/export.json").to_return(:status => 200, :body => invalid_json) |
|
106 |
+ stub_request(:get, "http://example.com/scenarios/1/export.json").to_return(:status => 200, :body => invalid_data) |
|
51 | 107 |
subject.url = "http://example.com/scenarios/1/export.json" |
52 | 108 |
subject.should_not be_valid |
53 | 109 |
subject.errors[:base].should include("The provided data does not appear to be a valid Scenario.") |
54 | 110 |
end |
55 | 111 |
|
56 | 112 |
it "should be valid when the url points to a valid scenario" do |
57 |
- stub_request(:get, "http://example.com/scenarios/1/export.json").to_return(:status => 200, :body => valid_json) |
|
113 |
+ stub_request(:get, "http://example.com/scenarios/1/export.json").to_return(:status => 200, :body => valid_data) |
|
58 | 114 |
subject.url = "http://example.com/scenarios/1/export.json" |
59 | 115 |
subject.should be_valid |
60 | 116 |
end |
@@ -66,15 +122,139 @@ describe ScenarioImport do |
||
66 | 122 |
subject.should_not be_valid |
67 | 123 |
subject.errors[:base].should include("The provided data does not appear to be a valid Scenario.") |
68 | 124 |
|
69 |
- subject.file = StringIO.new(invalid_json) |
|
125 |
+ subject.file = StringIO.new(invalid_data) |
|
70 | 126 |
subject.should_not be_valid |
71 | 127 |
subject.errors[:base].should include("The provided data does not appear to be a valid Scenario.") |
72 | 128 |
end |
73 | 129 |
|
74 | 130 |
it "should be valid with a valid uploaded scenario" do |
75 |
- subject.file = StringIO.new(valid_json) |
|
131 |
+ subject.file = StringIO.new(valid_data) |
|
76 | 132 |
subject.should be_valid |
77 | 133 |
end |
78 | 134 |
end |
79 | 135 |
end |
136 |
+ |
|
137 |
+ describe "#dangerous?" do |
|
138 |
+ it "returns false on most Agents" do |
|
139 |
+ ScenarioImport.new(:data => valid_data).should_not be_dangerous |
|
140 |
+ end |
|
141 |
+ |
|
142 |
+ it "returns true if a ShellCommandAgent is present" do |
|
143 |
+ valid_parsed_data[:agents][0][:type] = "Agents::ShellCommandAgent" |
|
144 |
+ ScenarioImport.new(:data => valid_parsed_data.to_json).should be_dangerous |
|
145 |
+ end |
|
146 |
+ end |
|
147 |
+ |
|
148 |
+ describe "#import!" do |
|
149 |
+ let(:scenario_import) do |
|
150 |
+ _import = ScenarioImport.new(:data => valid_data) |
|
151 |
+ _import.set_user users(:bob) |
|
152 |
+ _import |
|
153 |
+ end |
|
154 |
+ |
|
155 |
+ context "when this scenario has never been seen before" do |
|
156 |
+ it "makes a new scenario" do |
|
157 |
+ lambda { |
|
158 |
+ scenario_import.import!(:skip_agents => true) |
|
159 |
+ }.should change { users(:bob).scenarios.count }.by(1) |
|
160 |
+ |
|
161 |
+ scenario_import.scenario.name.should == name |
|
162 |
+ scenario_import.scenario.description.should == description |
|
163 |
+ scenario_import.scenario.guid.should == guid |
|
164 |
+ scenario_import.scenario.source_url.should == source_url |
|
165 |
+ scenario_import.scenario.public.should be_false |
|
166 |
+ end |
|
167 |
+ |
|
168 |
+ it "creates the Agents" do |
|
169 |
+ lambda { |
|
170 |
+ scenario_import.import! |
|
171 |
+ }.should change { users(:bob).agents.count }.by(2) |
|
172 |
+ |
|
173 |
+ weather_agent = scenario_import.scenario.agents.find_by(:guid => "a-weather-agent") |
|
174 |
+ trigger_agent = scenario_import.scenario.agents.find_by(:guid => "a-trigger-agent") |
|
175 |
+ |
|
176 |
+ weather_agent.name.should == "a weather agent" |
|
177 |
+ weather_agent.schedule.should == "5pm" |
|
178 |
+ weather_agent.keep_events_for.should == 14 |
|
179 |
+ weather_agent.propagate_immediately.should be_false |
|
180 |
+ weather_agent.should_not be_disabled |
|
181 |
+ weather_agent.memory.should be_empty |
|
182 |
+ weather_agent.options.should == weather_agent_options |
|
183 |
+ |
|
184 |
+ trigger_agent.name.should == "listen for weather" |
|
185 |
+ trigger_agent.sources.should == [weather_agent] |
|
186 |
+ trigger_agent.schedule.should be_nil |
|
187 |
+ trigger_agent.keep_events_for.should == 0 |
|
188 |
+ trigger_agent.propagate_immediately.should be_true |
|
189 |
+ trigger_agent.should be_disabled |
|
190 |
+ trigger_agent.memory.should be_empty |
|
191 |
+ trigger_agent.options.should == trigger_agent_options |
|
192 |
+ end |
|
193 |
+ |
|
194 |
+ it "creates new Agents, even if one already exists with the given guid (so that we don't overwrite a user's work outside of the scenario)" do |
|
195 |
+ agents(:bob_weather_agent).update_attribute :guid, "a-weather-agent" |
|
196 |
+ |
|
197 |
+ lambda { |
|
198 |
+ scenario_import.import! |
|
199 |
+ }.should change { users(:bob).agents.count }.by(2) |
|
200 |
+ end |
|
201 |
+ end |
|
202 |
+ |
|
203 |
+ context "when an a scenario already exists with the given guid" do |
|
204 |
+ let!(:existing_scenario) { |
|
205 |
+ _existing_scenerio = users(:bob).scenarios.build(:name => "an existing scenario") |
|
206 |
+ _existing_scenerio.guid = guid |
|
207 |
+ _existing_scenerio.save! |
|
208 |
+ _existing_scenerio |
|
209 |
+ } |
|
210 |
+ |
|
211 |
+ it "uses the existing scenario, updating it's data" do |
|
212 |
+ lambda { |
|
213 |
+ scenario_import.import!(:skip_agents => true) |
|
214 |
+ scenario_import.scenario.should == existing_scenario |
|
215 |
+ }.should_not change { users(:bob).scenarios.count } |
|
216 |
+ |
|
217 |
+ existing_scenario.reload |
|
218 |
+ existing_scenario.guid.should == guid |
|
219 |
+ existing_scenario.description.should == description |
|
220 |
+ existing_scenario.name.should == name |
|
221 |
+ existing_scenario.source_url.should == source_url |
|
222 |
+ existing_scenario.public.should be_false |
|
223 |
+ end |
|
224 |
+ |
|
225 |
+ it "updates any existing agents in the scenario, and makes new ones as needed" do |
|
226 |
+ agents(:bob_weather_agent).update_attribute :guid, "a-weather-agent" |
|
227 |
+ agents(:bob_weather_agent).scenarios << existing_scenario |
|
228 |
+ |
|
229 |
+ lambda { |
|
230 |
+ # Shouldn't matter how many times we do it! |
|
231 |
+ scenario_import.import! |
|
232 |
+ scenario_import.import! |
|
233 |
+ scenario_import.import! |
|
234 |
+ }.should change { users(:bob).agents.count }.by(1) |
|
235 |
+ |
|
236 |
+ weather_agent = existing_scenario.agents.find_by(:guid => "a-weather-agent") |
|
237 |
+ trigger_agent = existing_scenario.agents.find_by(:guid => "a-trigger-agent") |
|
238 |
+ |
|
239 |
+ weather_agent.should == agents(:bob_weather_agent) |
|
240 |
+ |
|
241 |
+ weather_agent.name.should == "a weather agent" |
|
242 |
+ weather_agent.schedule.should == "5pm" |
|
243 |
+ weather_agent.keep_events_for.should == 14 |
|
244 |
+ weather_agent.propagate_immediately.should be_false |
|
245 |
+ weather_agent.should_not be_disabled |
|
246 |
+ weather_agent.memory.should be_empty |
|
247 |
+ weather_agent.options.should == weather_agent_options |
|
248 |
+ |
|
249 |
+ trigger_agent.name.should == "listen for weather" |
|
250 |
+ trigger_agent.sources.should == [weather_agent] |
|
251 |
+ trigger_agent.schedule.should be_nil |
|
252 |
+ trigger_agent.keep_events_for.should == 0 |
|
253 |
+ trigger_agent.propagate_immediately.should be_true |
|
254 |
+ trigger_agent.should be_disabled |
|
255 |
+ trigger_agent.memory.should be_empty |
|
256 |
+ trigger_agent.options.should == trigger_agent_options |
|
257 |
+ end |
|
258 |
+ end |
|
259 |
+ end |
|
80 | 260 |
end |
@@ -1,54 +1,42 @@ |
||
1 | 1 |
require 'spec_helper' |
2 | 2 |
|
3 | 3 |
describe Scenario do |
4 |
+ let(:new_instance) { users(:bob).scenarios.build(:name => "some scenario") } |
|
5 |
+ |
|
6 |
+ it_behaves_like HasGuid |
|
7 |
+ |
|
4 | 8 |
describe "validations" do |
5 | 9 |
before do |
6 |
- @scenario = users(:bob).scenarios.new(:name => "some scenario") |
|
7 |
- @scenario.should be_valid |
|
10 |
+ new_instance.should be_valid |
|
8 | 11 |
end |
9 | 12 |
|
10 | 13 |
it "validates the presence of name" do |
11 |
- @scenario.name = '' |
|
12 |
- @scenario.should_not be_valid |
|
14 |
+ new_instance.name = '' |
|
15 |
+ new_instance.should_not be_valid |
|
13 | 16 |
end |
14 | 17 |
|
15 | 18 |
it "validates the presence of user" do |
16 |
- @scenario.user = nil |
|
17 |
- @scenario.should_not be_valid |
|
19 |
+ new_instance.user = nil |
|
20 |
+ new_instance.should_not be_valid |
|
18 | 21 |
end |
19 | 22 |
|
20 | 23 |
it "only allows Agents owned by user" do |
21 |
- @scenario.agent_ids = [agents(:bob_website_agent).id] |
|
22 |
- @scenario.should be_valid |
|
24 |
+ new_instance.agent_ids = [agents(:bob_website_agent).id] |
|
25 |
+ new_instance.should be_valid |
|
23 | 26 |
|
24 |
- @scenario.agent_ids = [agents(:jane_website_agent).id] |
|
25 |
- @scenario.should_not be_valid |
|
26 |
- end |
|
27 |
- end |
|
28 |
- |
|
29 |
- describe "guid" do |
|
30 |
- it "gets created before_save, but only if it's not present" do |
|
31 |
- scenario = users(:bob).scenarios.new(:name => "some scenario") |
|
32 |
- scenario.guid.should be_nil |
|
33 |
- scenario.save! |
|
34 |
- scenario.guid.should_not be_nil |
|
35 |
- |
|
36 |
- lambda { scenario.save! }.should_not change { scenario.reload.guid } |
|
27 |
+ new_instance.agent_ids = [agents(:jane_website_agent).id] |
|
28 |
+ new_instance.should_not be_valid |
|
37 | 29 |
end |
38 | 30 |
end |
39 | 31 |
|
40 | 32 |
describe "counters" do |
41 |
- before do |
|
42 |
- @scenario = users(:bob).scenarios.new(:name => "some scenario") |
|
43 |
- end |
|
44 |
- |
|
45 | 33 |
it "maintains a counter cache on user" do |
46 | 34 |
lambda { |
47 |
- @scenario.save! |
|
35 |
+ new_instance.save! |
|
48 | 36 |
}.should change { users(:bob).reload.scenario_count }.by(1) |
49 | 37 |
|
50 | 38 |
lambda { |
51 |
- @scenario.destroy |
|
39 |
+ new_instance.destroy |
|
52 | 40 |
}.should change { users(:bob).reload.scenario_count }.by(-1) |
53 | 41 |
end |
54 | 42 |
end |
@@ -0,0 +1,12 @@ |
||
1 |
+require 'spec_helper' |
|
2 |
+ |
|
3 |
+shared_examples_for HasGuid do |
|
4 |
+ it "gets created before_save, but only if it's not present" do |
|
5 |
+ instance = new_instance |
|
6 |
+ instance.guid.should be_nil |
|
7 |
+ instance.save! |
|
8 |
+ instance.guid.should_not be_nil |
|
9 |
+ |
|
10 |
+ lambda { instance.save! }.should_not change { instance.reload.guid } |
|
11 |
+ end |
|
12 |
+end |
@@ -3,7 +3,7 @@ require 'spec_helper' |
||
3 | 3 |
shared_examples_for WorkingHelpers do |
4 | 4 |
describe "recent_error_logs?" do |
5 | 5 |
it "returns true if last_error_log_at is near last_event_at" do |
6 |
- agent = Agent.new |
|
6 |
+ agent = described_class.new |
|
7 | 7 |
|
8 | 8 |
agent.last_error_log_at = 10.minutes.ago |
9 | 9 |
agent.last_event_at = 10.minutes.ago |
@@ -26,9 +26,10 @@ shared_examples_for WorkingHelpers do |
||
26 | 26 |
agent.recent_error_logs?.should be_false |
27 | 27 |
end |
28 | 28 |
end |
29 |
+ |
|
29 | 30 |
describe "received_event_without_error?" do |
30 | 31 |
before do |
31 |
- @agent = Agent.new |
|
32 |
+ @agent = described_class.new |
|
32 | 33 |
end |
33 | 34 |
|
34 | 35 |
it "should return false until the first event was received" do |
@@ -49,5 +50,4 @@ shared_examples_for WorkingHelpers do |
||
49 | 50 |
@agent.received_event_without_error?.should == true |
50 | 51 |
end |
51 | 52 |
end |
52 |
- |
|
53 | 53 |
end |